package cn.zyq.charging.order.service.impl;

import cn.zyq.charging.common.pojo.JsonResult;
import cn.zyq.charging.common.pojo.param.GunStatusUpdateParam;
import cn.zyq.charging.common.pojo.utils.JsonUtils;
import cn.zyq.charging.common.pojo.vo.StationInfoVO;
import cn.zyq.charging.common.pojo.vo.UserInfoVO;
import cn.zyq.charging.order.constant.Constant;
import cn.zyq.charging.order.constant.MqttConstant;
import cn.zyq.charging.order.feign.DeviceClient;
import cn.zyq.charging.order.feign.UserClient;
import cn.zyq.charging.order.quartz_job.DeviceCheckJob;
import cn.zyq.charging.order.mqtt.MqttProducer;
import cn.zyq.charging.order.pojo.dto.StationInfoDTO;
import cn.zyq.charging.order.pojo.dto.CarInfoDTO;
import cn.zyq.charging.order.pojo.dto.ChargingInfoDTO;
import cn.zyq.charging.order.pojo.param.OrderAddParam;
import cn.zyq.charging.order.pojo.po.OrderMQPO;
import cn.zyq.charging.order.rabbitmq.RabbitMQOrderProducer;
import cn.zyq.charging.order.service.OrderService;
import lombok.extern.slf4j.Slf4j;
import org.springframework.beans.BeanUtils;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.core.ParameterizedTypeReference;
import org.springframework.http.HttpMethod;
import org.springframework.http.ResponseEntity;
import org.springframework.stereotype.Service;
import org.springframework.web.client.RestTemplate;

import java.util.Random;

@Service
@Slf4j
public class OrderServiceImpl implements OrderService {

    @Autowired
    private RabbitMQOrderProducer rabbitMQOrderProducer;

    //调用rabbitMQOrderProducer的sendOrderMsg()方法发送发送OrderMQPO对象(里边封装了订单id)。
    private void sendOrderMsg(String orderNoStr) {
        OrderMQPO orderMQPO = new OrderMQPO();
        orderMQPO.setOrderNo(orderNoStr);
        rabbitMQOrderProducer.sendOrderMsg(orderMQPO);
        //用rabbitMQOrderProducer的sendOrderMsg()方法发送OrderMQPO对象(里边封装了订单id)
    }

    /**
     * 创建订单：
     *   1.用户先插入充电枪
     *   2.然后扫描二维码生成订单
     *   3.订单生成后，接着给充电柱发送充电指令。
     * @param orderAddParamDTO  入参对象
     * @return 订单id
     */
    public String createOrder(OrderAddParam orderAddParamDTO) throws  Exception{
        log.debug("创建订单方法： createOrder({})",orderAddParamDTO);
        //1.创建订单编号
        String orderNo = generateOrderNo();//本方法要返回订单编号
        //2.创建设备自检任务(传入订单id， 充电枪id)(1分钟自检，自检失败就通知用户更换设备，并通知运维人员检修)
        DeviceCheckJob deviceCheckJob=new DeviceCheckJob(orderNo, orderAddParamDTO.getGunId());
//        log.debug("生成的订单编号：{}",orderNo);
        //3.通过枪id获取运行商信息和充电桩信息
//        log.debug("通过枪id获取运行商信息和充电站信息入参：{}",orderAddParamDTO.getGunId());
        StationInfoDTO stationInfo = getStationInfoByGunId(orderAddParamDTO.getGunId());
//        log.debug("通过枪id获取运行商信息和充电站信息出参：{}",stationInfo);
        //3.通过用户id获取用户绑定的车辆信息
//        log.debug("通过用户id获取获取用户车辆信息-入参：{}",orderAddParamDTO.getUserId());
        CarInfoDTO carInfo = getCarInfoByUserId(orderAddParamDTO.getUserId());
//        log.debug("通过用户id获取获取用户车辆信息-出参：{}",carInfo);
        //4.通过枪id修改枪的状态为已使用 (2表示此充电枪 正在使用)
//        log.debug("通过枪id修改枪的状态用-入参：枪id{}，状态{}",orderAddParamDTO.getGunId(), Constant.GUN_STATS_BUSY);
        boolean statusRS=updateGunStatus(orderAddParamDTO.getGunId(),Constant.GUN_STATS_BUSY);
        log.debug("通过枪id修改枪的状态用-出参：枪id{}，状态{}，修改结果{}",orderAddParamDTO.getGunId(),Constant.GUN_STATS_BUSY,statusRS);
        if(!statusRS){
            log.warn("修改枪状态失败!,枪id:{},状态:{}",orderAddParamDTO.getGunId(),2);
        }

        //5 给充电桩发送充电指令(里边绑定了orderNo,GunId,PileId，指令msg)
        //(给充电桩用mqttProducer对象发送"/topic/start/充电桩ID" 消息给物联网中间件emqx-5.2.0-windows-amd64.exe)
        //(发送充电指令的ChargingInfoDTO对象应该和生成订单的方法没有关系，所以将ChargingInfoDTO对象放到startCharging()方法中)
        startCharging(orderNo,orderAddParamDTO.getPileId(),orderAddParamDTO.getGunId());

        //6.发送一个mq消息给RabbitMQ(消息是orderNO)，并设置延迟处理(设置超时时间)， 消息超时且没被消费掉，说明此订单出现异常。
        //就会将其转入到死信队列， 然后由死信队列的消费端消费此消息。则证明充电订单异常结束。 (11月11)
        sendOrderMsg(orderNo);
        return orderNo;
    }



    /**
     * 生成订单号
     * 10000 + "_" + 随机数 + "_" +时间戳
     * 为了避免多个用户同时创建订单(只是时间戳就有可能造成id重复)
     * return 订单号
     */
    private String generateOrderNo() {
        //雪花算法(自己了解)： 用来生成分布式id
        return "10000_" + (int)(Math.random()*101) + "_" + System.currentTimeMillis();
    }


    /**
     * 通过枪id获取运营商信息和充电站信息
     * @param gunId
     * @return StationInfoDTO  (DTO用于将数据返回给页面)
     */
    private StationInfoDTO getStationInfoByGunId(Integer gunId) {
        //TODO: 后边要改成用deviceClient调用getStationInfo()获取信息 //////////////////////////////
        //1.用deviceClient通过Feign调用getStationInfo(gunId): 根据充电枪id获取充电桩信息
        JsonResult<StationInfoVO> stationInfoVOJsonResult=deviceClient.getStationInfo(gunId);
        //2.如果能获取到：
        if(stationInfoVOJsonResult!=null){
            StationInfoVO stationInfoVOData=stationInfoVOJsonResult.getData();
            //2.1.则从查询的对象中获取充电桩信息对象StationInfoVO。
            if(stationInfoVOData!=null){
                //2.2.如果获取从查询的对象中获取到了充电桩信息对象就将其复制到一个新创建的StationInfoDTO对象中并返回。
                StationInfoDTO stationInfoDTO=new StationInfoDTO();
                //stationInfoVOData--copy-->stationInfoDTO
                BeanUtils.copyProperties(stationInfoVOData, stationInfoDTO);
                return stationInfoDTO;
            }
        }

        /*StationInfoDTO stationInfoDTO = new StationInfoDTO();
        stationInfoDTO.setOperatorId(1);
        stationInfoDTO.setPileId(1);
        stationInfoDTO.setStationId(1);*/
        return  null;
    }







    /**
     * 通过用户id获取听用户绑定的车辆信息
     * 假定一个用户只能绑定一个车辆
     * @param userId
     * @return
     */
    private CarInfoDTO getCarInfoByUserId(Integer userId){
        /*CarInfoDTO carInfoDTO = new CarInfoDTO();
        carInfoDTO.setCarId(1);
        return carInfoDTO;*/

        //用RestTemplate调用user模块的根据用户编号获取用户信息的方法(获取用户绑定的车辆id)
        //return getCarInfoByUserIdRestTemplate(userId);
        //用RFeign调用user模块的根据用户编号获取用户信息的方法(获取用户绑定的车辆id)
        return getCarInfoByUserIdFeign(userId);
    }












    //这种方式(resTemplate方式调用用户模块)只是在项目中测试了一下
    //(后边又作废了， 因为后边换成了OpenFeign的方式调用用户模块了)
    //通过RestTemplate的方式调用用户服务charging-user模块的UserController的getUserInfo()方法
    // 根据用户id查询用户绑定的车辆信息.....................
    @Autowired
    private RestTemplate restTemplate;

    /**
     * 通过RestTemplate的方式调用用户服务charging-user模块的UserController的getUserInfo()方法
     * 根据用户id查询用户绑定的车辆信息.....................
     * @param userId
     * @return CarInfoDTO对象
     */
//    private CarInfoDTO getCarInfoByUserIdRestTemplate(Integer userId){
//        /*CarInfoDTO carInfoDTO = new CarInfoDTO();
//        carInfoDTO.setCarId(1);
//        return carInfoDTO;*/
//
//
//        String serverUrl=chooseUrl();
//        ParameterizedTypeReference<JsonResult<UserInfoVO>> responseBody=
//                new ParameterizedTypeReference<JsonResult<UserInfoVO>>() { };
//
//        ResponseEntity<JsonResult<UserInfoVO>> exchange =
//                restTemplate.exchange(serverUrl, HttpMethod.GET, null, responseBody, userId);
//        // responseBody是请求处理完成后返回值的类型JsonResult<UserInfoVO>
//        //  null表示请求体无内容  userId是请求参数
//        // 获取响应正文
//        JsonResult<UserInfoVO> body = exchange.getBody();
//        if(body.getCode()==200){
//            UserInfoVO data = body.getData();
//            CarInfoDTO carInfoDTO = new CarInfoDTO();
//            carInfoDTO.setCarId(data.getCarId());
//            return carInfoDTO;
//        }else{
//            return null;
//        }
//    }



    /*private String chooseUrl(){
        //后期有时间，可以将这些url写到配置文件中，然后在这里获取使用。
        String url = "http://localhost:8083/user/info/{1}";
        String url1 = "http://localhost:8084/user/info/{1}";
        String url2 = "http://localhost:8085/user/info/{1}";

        //负载均衡 简单的随机实现
        String[] servers = new String[3];
        servers[0] = url;
        servers[1] = url1;
        servers[2] = url2;

        Random random = new Random();
        int r = random.nextInt(servers.length);
        String server = servers[r];
        return server;
    }*/



    /**用openFeign调用user模块的根据用户编号获取用户信息的方法(获取用户绑定的车辆id)*/
    @Autowired
    private UserClient userClient;
    //使用方式：在Order模块的启动类上添加@EnableFeignClients，在UserClient接口上添加@FeignClient("userService")
    /**
     * 通过Feign的方式调用用户服务
     * @param userId
     * @return CarInfoDTO对象
     */
    private CarInfoDTO getCarInfoByUserIdFeign(Integer userId){
        /*CarInfoDTO carInfoDTO = new CarInfoDTO();
        carInfoDTO.setCarId(1);
        return carInfoDTO;*/
        JsonResult<UserInfoVO>  userInfoVO = userClient.getUserInfo(userId);
        if(userInfoVO != null ){
            UserInfoVO data = userInfoVO.getData();
            if (data != null) {
                CarInfoDTO carInfoDTO = new CarInfoDTO();
                carInfoDTO.setCarId(data.getCarId());
                return carInfoDTO;
            }
        }
        return null;
    }

    /**用openFeign调用device模块的根据充电枪编号更新充电枪状态的方法
     *
     * */
    @Autowired
    private DeviceClient deviceClient;
    //使用方式：在Order模块的启动类上添加@EnableFeignClients，在DeviceClient接口上添加@FeignClient("deviceService")

    /**
     * 修改充电枪的状态为'已经使用'
     * @param gunId 枪id
     * @param gunStatusBusy 枪状态
     * @return
     */
    private Boolean updateGunStatus(Integer gunId, Integer gunStatusBusy){
        //1.创建一个更新状态的参数对象， 并给其设置充电枪的id，设置充电枪的是否占用的状态码。
        GunStatusUpdateParam gunStatusUpdateParam=new GunStatusUpdateParam();
        gunStatusUpdateParam.setGunId(gunId);
        gunStatusUpdateParam.setStatus(gunStatusBusy);
        //2.然后根据此更新状态的参数对象用deviceClient的updateGunStatus()调用设备模块charging-device的更新充电枪状态
        //todo : deviceClient.updateGunStatus()方法还未实现  xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx
        JsonResult<Boolean> booleanJsonResult=deviceClient.updateGunStatus(gunStatusUpdateParam);
        //3.如果收到更新结果了，就返回更新结果中包含的boolean值。
        if(booleanJsonResult!=null){
            return booleanJsonResult.getData();
        }//4.如果没有接收到更新结果对象，就返回false(表示更新失败)
        return false;
    }



    @Autowired
    private MqttProducer mqttProducer;
    /**
     * @param orderNo
     * @param pileId
     * @param gunId
     * 用来给充电桩发送  "开始充电"  指令的方法
     * (此方法应该是异步给充电桩发送指令的： 给充电桩发送指令后不需要等待充电桩的结果反馈，可以继续发送下一跳指令给另一个充电桩)
     */
//    private void startCharging(OrderAddParam orderAddParamDTO) {//这种方式有些不太符合要求(因为不需要用户id)
    private void startCharging(String orderNo, Integer pileId, Integer gunId) {
        log.debug("发送指令：订单号-{}， 充电桩id-{}，充电枪id-{}" , orderNo, pileId,gunId);
        //方法参数尽可能从大范围到小范围排序(可以避免传错参数)。
        ChargingInfoDTO chargingInfoDTO = new ChargingInfoDTO();
        chargingInfoDTO.setOrderNo(orderNo);
        chargingInfoDTO.setPileId(pileId);
        chargingInfoDTO.setGunId(gunId);
        chargingInfoDTO.setMsg(Constant.START_CHARGING);
        //Constant.START_CHARGING: start_charging


        //String chargingInfoDTOStr=chargingInfoDTO.toString();
        //用toString()将对象转化为String发送格式解析相对繁琐。
        //换成JSON工具，将chargingInfoDTO对象转化为json字符串发送出去解析更方便一些
        //创建一个工具类来实现将一个java对象转化为字符串的功能。
        String jsonChargingInfoDTO= JsonUtils.toJson(chargingInfoDTO);

        //给充电桩用mqttProducer对象发送"/topic/start/充电桩ID" 消息给物联网中间件emqx-5.2.0-windows-amd64.exe
        // (发送的消息后边要拼接充电桩的id，因为充电桩很多)
        // (TOPIC_START_CHARGING_PREFIX的具体指是: "/topic/start/")
        // (发送的消息： topic前缀拼接 + 充电桩id)
        mqttProducer.sendMessage(MqttConstant.TOPIC_START_CHARGING_PREFIX+chargingInfoDTO.getPileId(),jsonChargingInfoDTO);
        log.debug("OrderServiceImpl发送指令::"+MqttConstant.TOPIC_START_CHARGING_PREFIX+chargingInfoDTO.getPileId());
    }

}

//{
//	"orderNo": "10000_61_1724896612460",
//	"result":"start_success"
//}